namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Flags issues where the backing field is not the same as the property.
    /// </summary>
    public class BackingFieldShouldHaveSameNameAsProperty : BaseIntrospectionRule
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="BackingFieldShouldHaveSameNameAsProperty"/> class.
        /// </summary>
        public BackingFieldShouldHaveSameNameAsProperty()
            : base(typeof(BackingFieldShouldHaveSameNameAsProperty).Name, "Jsl.FxCop.Rules", typeof(BackingFieldShouldHaveSameNameAsProperty).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Checks the specified member.
        /// </summary>
        /// <param name="member">The member.</param>
        /// <returns>The problems found.</returns>
        public override ProblemCollection Check(Member member)
        {
            // Only process properties.
            var property = member as PropertyNode;
            if (property == null)
            {
                return this.Problems;
            }

            // Ignore generated code.
            if (property.DeclaringType.IsGeneratedCode())
            {
                return this.Problems;
            }

            // Get the possible backing fields for this property.
            var possibleFields = GetPossibleBackingFields(property).ToArray();
            if (possibleFields == null)
            {
                return this.Problems;
            }

            // Search for a field with a matching name.
            var matchingField =
                (from field in possibleFields
                 where IsMatchingBackingName(property.Name.Name, field.Name.Name)
                 select field)
                .FirstOrDefault();

            // If there were no matching fields but one or more fields were used then add the problem.
            if ((matchingField == null)
                && (possibleFields.Length > 0))
            {
                var messageId = (possibleFields.Length == 1) ? "OneFound" : "MultipleFound";
                var firstEntry = possibleFields[0];
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution(
                            messageId,
                            property.Name.Name,
                            firstEntry.Name.Name),
                        member.SourceContext));
            }

            return this.Problems;
        }
        #endregion Public Methods

        #region Private Methods
        private static IEnumerable<Field> GetPossibleBackingFields(PropertyNode property)
        {
            var getterFields = GetFieldInstructions(
                property.Getter,
                property.IsStatic ? OpCode.Ldsfld : OpCode.Ldfld);

            if (property.Setter != null)
            {
                var setterFields = GetFieldInstructions(
                    property.Setter,
                    property.IsStatic ? OpCode.Stsfld : OpCode.Stfld);

                // Get all the fields that are set in the setter AND retrieved in the getter.
                return setterFields.Intersect(getterFields);
            }
            else
            {
                // There is only a getter so return all fields that are retrieved in the getter.
                return getterFields;
            }
        }

        private static IEnumerable<Field> GetFieldInstructions(Method getterOrSetter, OpCode opCodeToLookFor)
        {
            if (getterOrSetter == null)
            {
                return new Field[] { };
            }

            // Get all the load field instructions that are loading a field declared in the getter's Type.
            return
                from instruction in getterOrSetter.Instructions
                where instruction.OpCode == opCodeToLookFor
                let field = (Field)instruction.Value
                where field.DeclaringType == getterOrSetter.DeclaringType
                select field;
        }

        private static bool IsMatchingBackingName(string visibleName, string backingName)
        {
            if (backingName.StartsWith("m_", StringComparison.Ordinal))
            {
                // Strip the "m_" of the front if there is one.
                backingName = backingName.Substring(2);
            }
            else if (backingName.StartsWith("_", StringComparison.Ordinal))
            {
                // Strip the "_" of the front if there is one.
                backingName = backingName.Substring(1);
            }

            // The visibleName should start uppercase and the backing name start lowercase.
            if (!char.IsUpper(visibleName[0]) || char.IsUpper(backingName[0]))
            {
                return false;
            }

            return (char.ToUpper(backingName[0], CultureInfo.InvariantCulture) == visibleName[0])
                && (backingName.Substring(1) == visibleName.Substring(1));
        }
        #endregion Private Methods
    }
}